//
//  JRFullscreenPopGesture.swift
//  LEDOTA
//
//  Created by rui on 2024/7/11.
//

import UIKit
import WebKit

@objc
public class JRFullscreenPopGesture: NSObject {
    
    @objc public static var gestureType: JRFullscreenPopGestureType = .edgeLeft
    
    @objc public static var transitionMode: JRFullscreenPopGestureTransitionMode = .shifting
    
    @objc public static var maxOffsetToTriggerPop: CGFloat = 0.35
    
    @objc public enum JRFullscreenPopGestureType: Int {
        
        case edgeLeft
        
        case full
    }
    
    @objc public enum JRFullscreenPopGestureTransitionMode: Int {
        
        case shifting
        
        case maskAndShifting
    }

    @objc public class func install() -> Void {
        if ( installed == true ) {
            return;
        }
        
        installed = true
        
        let cls = UINavigationController.self
        let originalSelector = #selector(UINavigationController.pushViewController(_:animated:))
        let swizzledSelector = #selector(UINavigationController.jr_pushViewController(_:animated:))
        let originalMethod = class_getInstanceMethod(cls, originalSelector)!;
        let swizzledMethod = class_getInstanceMethod(cls, swizzledSelector)!;
        method_exchangeImplementations(originalMethod, swizzledMethod)
    }
    
    private static var installed: Bool = false

}

@objc
public extension UIViewController {
    
    var jr_displayMode: JRPreViewDisplayMode {
        get {
            withUnsafePointer(to: &JRAssociatedKeys.displayMode) {
                return (objc_getAssociatedObject(self, UnsafeRawPointer($0)) as? JRPreViewDisplayMode) ?? .snapshot
            }
        }
        
        set {
            edgesForExtendedLayout = UIRectEdge.init()
            withUnsafePointer(to: &JRAssociatedKeys.displayMode) {
                objc_setAssociatedObject(self, UnsafeRawPointer($0), newValue, .OBJC_ASSOCIATION_RETAIN_NONATOMIC)
            }
        }
    }
    
    var jr_disableFullscreenGesture: Bool {
        get {
            withUnsafePointer(to: &JRAssociatedKeys.disableFullscreenGesture) {             
                return (objc_getAssociatedObject(self, UnsafeRawPointer($0)) as? Bool) ?? false
            }
        }
        
        set {
            withUnsafePointer(to: &JRAssociatedKeys.disableFullscreenGesture) {
                objc_setAssociatedObject(self, UnsafeRawPointer($0), newValue, .OBJC_ASSOCIATION_RETAIN_NONATOMIC)
            }
        }
    }
    
    var jr_blindArea: [CGRect]? {
        get {
            withUnsafePointer(to: &JRAssociatedKeys.blindArea) {
                return objc_getAssociatedObject(self, UnsafeRawPointer($0)) as? [CGRect]
            }
        }
        
        set {
            withUnsafePointer(to: &JRAssociatedKeys.blindArea) {
                objc_setAssociatedObject(self, UnsafeRawPointer($0), newValue, .OBJC_ASSOCIATION_RETAIN_NONATOMIC)
            }
        }
    }
    
    var jr_blindAreaViews: [UIView]? {
        get {
            withUnsafePointer(to: &JRAssociatedKeys.blindAreaViews) {
                return objc_getAssociatedObject(self, UnsafeRawPointer($0)) as? [UIView]
            }
        }
        
        set {
            withUnsafePointer(to: &JRAssociatedKeys.blindAreaViews) {
                objc_setAssociatedObject(self, UnsafeRawPointer($0), newValue, .OBJC_ASSOCIATION_RETAIN_NONATOMIC)
            }
        }
    }
    
    var jr_viewWillBeginDragging: ((UIViewController) -> Void)? {
        get {
            withUnsafePointer(to: &JRAssociatedKeys.viewWillBeginDragging) {
                return objc_getAssociatedObject(self, UnsafeRawPointer($0)) as? ((UIViewController) -> Void)
            }
        }
        
        set {
            withUnsafePointer(to: &JRAssociatedKeys.viewWillBeginDragging) {
                objc_setAssociatedObject(self, UnsafeRawPointer($0), newValue, .OBJC_ASSOCIATION_RETAIN_NONATOMIC)
            }
        }
    }
    
    var jr_viewDidDrag: ((UIViewController) -> Void)? {
        get {
            withUnsafePointer(to: &JRAssociatedKeys.viewDidDrag) {
                return objc_getAssociatedObject(self, UnsafeRawPointer($0)) as? ((UIViewController) -> Void)
            }
        }
        
        set {
            withUnsafePointer(to: &JRAssociatedKeys.viewDidDrag) {
                objc_setAssociatedObject(self, UnsafeRawPointer($0), newValue, .OBJC_ASSOCIATION_RETAIN_NONATOMIC)
            }
        }
    }
    
    var jr_viewDidEndDragging: ((UIViewController) -> Void)? {
        get {
            withUnsafePointer(to: &JRAssociatedKeys.viewDidEndDragging) {
                return objc_getAssociatedObject(self, UnsafeRawPointer($0)) as? ((UIViewController) -> Void)
            }
        }
        
        set {
            withUnsafePointer(to: &JRAssociatedKeys.viewDidEndDragging) {
                objc_setAssociatedObject(self, UnsafeRawPointer($0), newValue, .OBJC_ASSOCIATION_RETAIN_NONATOMIC)
            }
        }
    }
    
    var jr_considerWebView: WKWebView? {
        get {
            withUnsafePointer(to: &JRAssociatedKeys.considerWebView) {
                return objc_getAssociatedObject(self, UnsafeRawPointer($0)) as? WKWebView
            }
        }
        
        set {
            newValue?.allowsBackForwardNavigationGestures = true
            withUnsafePointer(to: &JRAssociatedKeys.considerWebView) {
                objc_setAssociatedObject(self, UnsafeRawPointer($0), newValue, .OBJC_ASSOCIATION_RETAIN_NONATOMIC)
            }
        }
    }

    
    /// 返回时, 前视图的显示模式
    ///
    /// - snapshot: use screenshot view
    /// - origin: use origin view. If you use it, I will change `edgesForExtendedLayout` to `none` of viewController. 采用原始图, 如果使用它, 我会改变控制器的 edgesForExtendedLayout 为 none.
    @objc enum JRPreViewDisplayMode: Int {
        
        case snapshot
        
        case origin
    }
    
}

fileprivate extension UIViewController {
    var jr_previousViewControllerSnapshot: JRSnapshot? {
        get {
            withUnsafePointer(to: &JRAssociatedKeys.snapshot) {
                return objc_getAssociatedObject(self, UnsafeRawPointer($0)) as? JRSnapshot
            }
        }
        
        set {
            withUnsafePointer(to: &JRAssociatedKeys.snapshot) {
                objc_setAssociatedObject(self, UnsafeRawPointer($0), newValue, .OBJC_ASSOCIATION_RETAIN_NONATOMIC)
            }
        }
    }
}

@objc
public extension UINavigationController {
    var jr_fullscreenGestureState: UIGestureRecognizer.State {
        get {
            return self.jr_fullscreenGesture.state
        }
    }
}

fileprivate extension UINavigationController {

    @objc func jr_pushViewController(_ viewController: UIViewController, animated: Bool) {
        jr_setupIfNeeded()
        JRTransitionHandler.shared.push(self, viewController)
        jr_pushViewController(viewController, animated: animated)
    }
    
    private func jr_setupIfNeeded() {
        if ( self.interactivePopGestureRecognizer == nil ) {
            return
        }
        
        let tookOver: Bool = withUnsafePointer(to: &JRAssociatedKeys.tookOver) {
            return (objc_getAssociatedObject(self, UnsafeRawPointer($0)) as? Bool) ?? false
        }
        
        if ( tookOver == true ) {
            return
        }
        
        withUnsafePointer(to: &JRAssociatedKeys.tookOver) {
            objc_setAssociatedObject(self, UnsafeRawPointer($0), true, .OBJC_ASSOCIATION_RETAIN_NONATOMIC)
        }
        
        self.interactivePopGestureRecognizer?.isEnabled = false
        self.view.clipsToBounds = false
        
        // gesture
        view.addGestureRecognizer(jr_fullscreenGesture)
    }
    
    var jr_fullscreenGesture: UIPanGestureRecognizer {
        get {
            if let gesture = withUnsafePointer(to: &JRAssociatedKeys.fullscreenGesture, {
                return objc_getAssociatedObject(self, UnsafeRawPointer($0)) as? UIPanGestureRecognizer
            }) {
                return gesture
            }
            
            let gesture: UIPanGestureRecognizer = JRFullscreenPopGesture.gestureType == .edgeLeft ? UIScreenEdgePanGestureRecognizer.init() : UIPanGestureRecognizer.init()
            gesture.delaysTouchesBegan = true
            gesture.delegate = JRFullscreenPopGestureDelegate.shared
            gesture.addTarget(self, action: #selector(jr_handleFullscreenGesture(_:)))
            if JRFullscreenPopGesture.gestureType == .edgeLeft {
                (gesture as! UIScreenEdgePanGestureRecognizer).edges = .left
            }
            withUnsafePointer(to: &JRAssociatedKeys.fullscreenGesture, {
                objc_setAssociatedObject(self, UnsafeRawPointer($0), gesture, .OBJC_ASSOCIATION_RETAIN_NONATOMIC)
            })
            return gesture
        }
    }
    
    @objc private func jr_handleFullscreenGesture(_ pan: UIPanGestureRecognizer) {
        guard let viewController = topViewController else {
            return
        }
        
        let offset = pan.translation(in: self.view).x
        
        switch pan.state {
        case .began:
            JRTransitionHandler.shared.began(self, viewController, offset)
        case .changed:
            JRTransitionHandler.shared.changed(self, viewController, offset)
        case .ended, .cancelled, .failed:
            JRTransitionHandler.shared.completed(self, viewController, offset)
        case .possible: break
        @unknown default: break
        }
    }
}




fileprivate class JRFullscreenPopGestureDelegate: NSObject, UIGestureRecognizerDelegate {
 
    static let shared: JRFullscreenPopGestureDelegate = JRFullscreenPopGestureDelegate.init()
    
    func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldReceive touch: UITouch) -> Bool {
        guard let nav = _lookupResponder(gestureRecognizer.view!, UINavigationController.self), nav.children.count > 1 else {
            return false
        }
        
        if let isTransitioning = nav.value(forKey: JRAssociatedKeys.isTransitioning) as? Bool, isTransitioning == true {
            return false
        }
        
        if ( nav.topViewController!.jr_disableFullscreenGesture ) {
            return false
        }
        
        if _blindAreaContains(nav, touch.location(in: nav.view)) {
            return false
        }
        
        if let last = nav.children.last, last.isKind(of: UINavigationController.self) {
            return false
        }
        
        if let webView = nav.topViewController!.jr_considerWebView {
            return !webView.canGoBack;
        }
        
        return true
    }

    
    func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool {
        
        if JRFullscreenPopGesture.gestureType == .edgeLeft {
            return true
        }

        let gesture = gestureRecognizer as! UIPanGestureRecognizer
        
        let translate = gesture.translation(in: gesture.view)
        
        if ( translate.x > 0 && translate.y == 0 ) {
            return true
        }
        
        return false
    }
    
    // 全屏手势是否和other手势一起触发
    func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool {

        if ( gestureRecognizer.state == .failed ||
             gestureRecognizer.state == .cancelled ) {
            return false
        }

        if JRFullscreenPopGesture.gestureType == .edgeLeft {
            _cancelGesture(otherGestureRecognizer)
            return true
        }
        
        guard let view = gestureRecognizer.view else {
            return false
        }
        
        guard let nav = _lookupResponder(view, UINavigationController.self) else {
            return false
        }
        
        let location = gestureRecognizer.location(in: view)
        
        if _blindAreaContains(nav, location) {
            return false
        }

        // scroll view
        if let cla = NSClassFromString("UIScrollViewPanGestureRecognizer"), cla != nil, let cla1 = NSClassFromString("UIScrollViewPagingSwipeGestureRecognizer"), cla1 != nil {
            if ( otherGestureRecognizer.isMember(of: cla) ||
                 otherGestureRecognizer.isMember(of: cla1) ) {
                if let scrollView = otherGestureRecognizer.view as? UIScrollView {
                    return _shouldRecognizeSimultaneously(scrollView, nav.jr_fullscreenGesture, otherGestureRecognizer)
                }
            }
        }
        
        if let cla = NSClassFromString("_MKMapContentView"), let cla1 = NSClassFromString("UIWebTouchEventsGestureRecognizer"), cla != nil, cla1 != nil {
            if (otherGestureRecognizer.view?.isKind(of: cla) ?? false) || otherGestureRecognizer.isKind(of: cla1) {
                if _edgeAreaContains(nav, location) {
                    _cancelGesture(otherGestureRecognizer)
                    return true
                }
                else {
                    return false
                }
            }
        }
                
        if otherGestureRecognizer.isKind(of: UIPanGestureRecognizer.self) {
            return false
        }
        
        return false
    }
    
    func _shouldRecognizeSimultaneously(_ scrollView: UIScrollView, _ gestureRecognizer: UIPanGestureRecognizer, _ otherGestureRecognizer: UIGestureRecognizer) -> Bool {
        
        // UIPageViewController
        if let cla = NSClassFromString("_UIQueuingScrollView"), cla != nil, scrollView.isKind(of: cla) {
            if ( scrollView.isDecelerating ) {
                return false
            }
            
            guard let pageVC = _lookupResponder(scrollView, UIPageViewController.self) else {
                return false
            }
            
            guard let dataSource = pageVC.dataSource else {
                return false
            }
            
            guard let topViewController = pageVC.viewControllers?.first else {
                return false
            }
            
            if dataSource.pageViewController(pageVC, viewControllerBefore: topViewController) != nil {
                return false
            }
            
            _cancelGesture(otherGestureRecognizer)
            
            return true
        }
        
        let translate = gestureRecognizer.translation(in: gestureRecognizer.view!)
        
        if ( 0 == scrollView.contentOffset.x + scrollView.contentInset.left
            && !scrollView.isDecelerating
            && translate.x > 0 && 0 == translate.y ) {
            
            _cancelGesture(otherGestureRecognizer)
            
            return true
        }
        
        return false
    }
    
    func _edgeAreaContains(_ nav: UINavigationController, _ point: CGPoint) -> Bool {
        let offset: CGFloat = 50
        let rect = CGRect.init(origin: CGPoint.init(x: 0, y: 0), size: CGSize.init(width: offset, height: nav.view.bounds.height))
        
        return _rectContains(nav, rect, point, false)
    }
    
    func _blindAreaContains(_ nav: UINavigationController, _ point: CGPoint) -> Bool {
        if let blindArea = nav.topViewController?.jr_blindArea {
            for rect in blindArea {
                if _rectContains(nav, rect, point, true) {
                    return true
                }
            }
        }
        
        if let blindAreaViews = nav.topViewController?.jr_blindAreaViews {
            for view in blindAreaViews {
                if _rectContains(nav, view.frame, point, true) {
                    return true
                }
            }
        }
    
        return false
    }
    
    func _rectContains(_ nav: UINavigationController, _ rect: CGRect, _ point: CGPoint, _ shouldConvertRect: Bool) -> Bool {
        var value = rect
        
        if shouldConvertRect {
            value = nav.topViewController!.view.convert(rect, to: nav.view)
        }
        
        return value.contains(point)
    }
    
    func _lookupResponder<T>(_ first: UIResponder, _ cls: T.Type) -> T? {
        var next: UIResponder? = first
        while next != nil && next?.isKind(of: cls as! AnyClass) == false {
            next = next?.next
        }
        return next as? T
    }
    
    func _cancelGesture(_ gesture: UIGestureRecognizer) {
        gesture.setValue(UIGestureRecognizer.State.cancelled.rawValue, forKey: "state")
    }
}

fileprivate class JRTransitionBackgroundView : UIView {
    override init(frame: CGRect) {
        super.init(frame: frame)
        backgroundColor = .white
        layer.shadowOffset = .init(width: 0.5, height: 0)
        layer.shadowColor = UIColor(white: 0.2, alpha: 1).cgColor
        layer.shadowOpacity = 1
        layer.shadowRadius = 2
    }
    
    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
    
    override func layoutSubviews() {
        super.layoutSubviews()
        layer.shadowPath = UIBezierPath(roundedRect: bounds, cornerRadius: layer.shadowRadius).cgPath
    }
}

fileprivate class JRTransitionHandler {
    static let shared: JRTransitionHandler = JRTransitionHandler.init()
    
    let shift: CGFloat = -UIScreen.main.bounds.width * 0.382
    let backgroundView = JRTransitionBackgroundView.init(frame: .zero)
    
    func push(_ nav: UINavigationController, _ viewController: UIViewController) {
        if let last = nav.children.last {
            viewController.jr_previousViewControllerSnapshot = JRSnapshot.init(target: last)
        }
    }
    
    func began(_ nav: UINavigationController, _ viewController: UIViewController, _ offset: CGFloat) {
        //
        guard let snapshot = viewController.jr_previousViewControllerSnapshot else {
            return
        }
        
        // keyboard
        nav.view.endEditing(true)
        nav.view.superview?.insertSubview(snapshot.rootView, belowSubview: nav.view)
        nav.view.insertSubview(backgroundView, at: 0)
        backgroundView.frame = nav.view.bounds
        
        //
        snapshot.began()
        
        snapshot.rootView.transform = CGAffineTransform.init(translationX: self.shift, y: 0)
        
        if ( JRFullscreenPopGesture.transitionMode == .maskAndShifting ) {
            snapshot.maskView?.alpha = 1
            let width = snapshot.rootView.frame.width
            snapshot.maskView?.transform = CGAffineTransform.init(translationX: -(self.shift + width), y: 0)
        }
        
        //
        if let block = viewController.jr_viewWillBeginDragging {
            block(viewController)
        }
        
        changed(nav, viewController, offset)
    }
    
    func changed(_ nav: UINavigationController, _ viewController: UIViewController, _ offset: CGFloat) {
        //
        guard let snapshot = viewController.jr_previousViewControllerSnapshot else {
            return
        }

        //
        var value = offset
        if ( offset < 0 ) {
            value = 0;
        }
        
        //
        nav.view.transform = CGAffineTransform.init(translationX: value, y: 0)
        
        //
        let width = snapshot.rootView.frame.width
        let rate = offset / width

        snapshot.rootView.transform = CGAffineTransform.init(translationX: self.shift * ( 1 - rate ), y: 0)
        
        if ( JRFullscreenPopGesture.transitionMode == .maskAndShifting ) {
            snapshot.maskView?.alpha = 1 - rate
            snapshot.maskView?.transform = CGAffineTransform.init(translationX: -( self.shift + width ) + ( self.shift * rate ) + offset , y: 0 )
        }
        
        //
        if let block = viewController.jr_viewDidDrag {
            block(viewController)
        }
    }
    
    func completed(_ nav: UINavigationController, _ viewController: UIViewController, _ offset: CGFloat) {
        //
        guard let snapshot = viewController.jr_previousViewControllerSnapshot else {
            return
        }

        //
        let screenWidth = nav.view.frame.width
        let rate = offset / screenWidth
        let maxOffset = JRFullscreenPopGesture.maxOffsetToTriggerPop
        let shouldPop = rate > maxOffset
        var animDuration = CGFloat.init(0.25)
        if ( shouldPop == false ) {
            animDuration = animDuration * ( offset / (maxOffset * screenWidth) ) + 0.05;
        }
        
        UIView.animate(withDuration: TimeInterval(animDuration), animations: {
            if ( shouldPop == true ) {
                snapshot.rootView.transform = CGAffineTransform.identity
                snapshot.maskView?.transform = CGAffineTransform.identity
                snapshot.maskView?.alpha = 0.001
                
                nav.view.transform = CGAffineTransform.init(translationX: nav.view.frame.width, y: 0)
            }
            else {
                snapshot.maskView?.alpha = 1
                snapshot.maskView?.transform = CGAffineTransform.init(translationX: -(self.shift + screenWidth), y: 0)
                
                nav.view.transform = CGAffineTransform.identity
            }
        }) { (_) in
            self.backgroundView.removeFromSuperview()
            snapshot.rootView.removeFromSuperview()
            snapshot.completed()
            
            if ( shouldPop == true ) {
                nav.view.transform = CGAffineTransform.identity
                nav.popViewController(animated: false)
            }
            if let block = viewController.jr_viewDidEndDragging {
                block(viewController)
            }
        }
    }
}

fileprivate class JRSnapshot {
    
    weak var target: UIViewController?
    
    var rootView: UIView
    
    var maskView: UIView?
    
    init(target: UIViewController) {
        // target
        self.target = target
        
        // nav
        let nav = target.navigationController!
        rootView = UIView.init(frame: nav.view.bounds)
        rootView.backgroundColor = .clear
        
        // snapshot
        switch target.jr_displayMode {
        case .snapshot:
            // snapshaot
            if let superview = nav.tabBarController != nil ? nav.tabBarController?.view : nav.view.superview {
                if let snapshot = superview.snapshotView(afterScreenUpdates: false) {
                    rootView.addSubview(snapshot)
                }
            }
        case .origin:
            // nav bar snapshot
            if nav.isNavigationBarHidden == false {
                var rect = nav.view.convert(nav.navigationBar.frame, to: nav.view.window)
                rect.size.height += rect.origin.y + 1;
                rect.origin.y = 0;
                if let snapshot = nav.view.superview?.resizableSnapshotView(from: rect, afterScreenUpdates: false, withCapInsets: UIEdgeInsets.zero) {
                    snapshot.frame = rect
                    rootView.addSubview(snapshot)
                }
            }
            
            // tab bar snapshot
            if let tabBar = nav.tabBarController?.tabBar, tabBar.isHidden == false {
                var rect = tabBar.convert(tabBar.bounds, to: nav.view.window)
                rect.origin.y -= 1
                rect.size.height += 1
                if let snapshot = nav.view.window?.resizableSnapshotView(from: rect, afterScreenUpdates: false, withCapInsets: UIEdgeInsets.zero) {
                    snapshot.frame = rect
                    rootView.addSubview(snapshot)
                }
            }
        }
        
        // mask
        if ( JRFullscreenPopGesture.transitionMode == .maskAndShifting ) {
            maskView = UIView.init(frame: rootView.bounds)
            maskView?.backgroundColor = UIColor.init(white: 0.0, alpha: 0.8)
            rootView.addSubview(maskView!)
        }
    }
    
    func began() {
        if let vc = target, vc.jr_displayMode == .origin {
            rootView.insertSubview(vc.view, at: 0)
        }
    }
    
    func completed() {
        guard let vc = target else {
            return
        }
        
        if ( vc.jr_displayMode == .origin && vc.view.superview == rootView ) {
            vc.view.removeFromSuperview()
        }
    }
}

fileprivate struct JRAssociatedKeys {
    static var tookOver = "khr_TookOver"
    static var displayMode = "kjr_DisplayMode"
    static var snapshot = "kjr_Snapshot"
    static var viewWillBeginDragging = "kjr_viewWillBeginDragging"
    static var viewDidDrag = "kjr_viewDidDrag"
    static var viewDidEndDragging = "kjr_viewDidEndDragging"
    static var disableFullscreenGesture = "kjr_disableFullscreenGesture"
    static var considerWebView = "kjr_considerWebView"
    static var blindArea = "kjr_blindArea"
    static var blindAreaViews = "kjr_blindAreaViews"
    
    static var isTransitioning = "_isTransitioning"
    static var fullscreenGesture = "kjr_FullscreenGesture"
}
